首先我们需要理解,内存是什么。简单来讲,内存存储了计算机运行过程的需要的全部数据,也就是计算机正在使用的全部数据。我们需要合理的使用内存,防止内存被大量无用数据占用,同时也要防止访问和修改与当前程序无关的内存区域。内存主要包括以下几个部分: 内核数据区域,栈区,共享库映像,堆区,可读写区域,只读区域。学习javascript,我们不需要理解内存和cache,内存和I/O之间具体工作原理,但我们需要了解掌握如何合理的使用内存,合理的分配释放内存。

javascript的内存管理

Javascript 是那些被称作垃圾回收语言当中的一员。垃圾回收语言通过周期性地检查那些之前被分配出去的内存是否可以从应用的其他部分访问来帮助开发者管理内存。换句话说,当计算机发现有的内存已经不能被访问到了,就会把它们标记为垃圾。开发者只需要知道一块已分配的内存是否会在将来被使用,而不可访问的内存可以通过算法确定并标记以便返还给操作系统。

引用传递和值传递

js中的变量除了6个基本类型以外,其余的都是对象。也就说基本类型在赋值是传递的是值,也就是原来数据的一份拷贝。基本类型包括number、string、boolean、symbol、null、undefined.
用2个例子来理解一下:

值传递

var a = 10;  //基本类型
var b = a;   //a把10拷贝一份,把这个拷贝给b
a = 20;  //修改了a,不影响a的拷贝
console.log(a);  //20
console.log(b);  //10

引用传递

var a = {num: 20};  //不是基本类型
var b = a;   //这里没有任何拷贝工作,b指向和a完全一致的同一块内存
b.num = 15;  //由于b和a指向同一块内存,所以b.num修改了等同于a.num修改了
console.log(a.num); //15
console.log(b.num); //15

//进一步理解
b = {age: 10};  //等号右边定义了一个新的对象,产生的新的内存分配,此时b指向了这块新的内存,a还是指向原来那块内存
console.log(a);  //{num: 15}
console.log(b);  //{age: 10}

值得强调的是:在函数参数传递的时候和返回值时一样遵守这个传递规则,这是构成闭包的基础条件

简单的函数传递参数

//一个反例
var num1 = 10;
var num2 = 20;
function swap(a, b){
    var temp = a;
    a = b;
    b = temp;
}
swap(num1, num2);
console.log(num1);  //10
console.log(num2);  //20

以上代码不能如愿的把2个传入变量的值交换,因为基本类型在参数传递时也是值传递,及a,b是num1,num2的拷贝,不是num1和num2本身。当然实现交换的方法很多,在不引入第三个变量情况下,不用单独写一个函数。

//实现交换a,b
//方法1:
var temp = a;
a = b;
b =temp;

//方法2:
a = a + b;
b = a - b;
a = a - b;

//方法3:
a = [b, b = a][0];

//方法4(仅适用于整数交换):
a = a ^ b; // ^表示异或运算
b = a ^ b; 
a = a ^ b; 

//方法5:
[a, b] = [b, a]; //解构赋值

闭包的原理

var inc = function(){
    var x = 0;
    return function(){  //返回一个非基本类型
        console.log(x++);
    };
};
inc1 = inc();  //inc1是闭包内匿名函数的引用,由于该引用存在,匿名函数引用计数不为0,所以inc作用域对应的内存不能释放,闭包形成
inc1();  //0
inc1();  //1

浅拷贝与深拷贝

当对象的属性是对象的时候,简单地赋值导致改属性传递的是另一个对象属性的引用,这样的拷贝是浅拷贝,存在安全风险。我们应该递归的拷贝对象属性的每个对象,形成深拷贝。方法如下:

//浅拷贝与深拷贝
var o = {
    name: "Lily",
    age: 10,
    addr:{
        city: "Shenzheng",
        province: "Guangdong"
    },
    schools: ["primaryS", "middleS", "heightS"]
};
var newOne = copy(o);
console.log(o);   //Object {name: "Lily", age: 10, addr: Object}
console.log(newOne);   //Object {name: "Lily", age: 10, addr: Object}

newOne.name = "Bob";
console.log(newOne.name);  //"Bob"
console.log(o.name);   //"Lily"

newOne.addr.city = "Foshan";
console.log(newOne.addr.city);   //"Foshan"
console.log(o.addr.city);   //"Foshan"

function copy(obj){
    var obj = obj || {};
    var newObj = {};
    for(prop in obj){
        if(!obj.hasOwnProperty(prop)) continue;
        newObj[prop] = obj[prop];  //当obj[prop]不是基本类型的时候,这里传的时引用
    }
    return newObj;
}

var newOne = deepCopy(o);
console.log(o);   //Object {name: "Lily", age: 10, addr: Object}
console.log(newOne);   //Object {name: "Lily", age: 10, addr: Object}

newOne.name = "Bob";
console.log(newOne.name);  //"Bob"
console.log(o.name);   //"Lily"

newOne.addr.city = "Foshan";
console.log(newOne.addr.city);   //"Foshan"
console.log(o.addr.city);   //"Shenzheng"

newOne.schools[0] = "primatrySchool";
console.log(newOne.schools[0]);   //"primatrySchool"
console.log(o.schools[0]);   //"primatryS"

function deepCopy(obj){
    var obj = obj || {};
    var newObj = {};
    deeply(obj, newObj);
    
    function deeply(oldOne, newOne){
        for(var prop in oldOne){
            if(!oldOne.hasOwnProperty(prop)) continue;
            if(typeof oldOne[prop] === "object" && oldOne[prop] !== null){
                newOne[prop] = oldOne[prop].constructor === Array ? [] : {};
                deeply(oldOne[prop], newOne[prop]);
            }
            else
                newOne[prop] = oldOne[prop];
        }
    }
    return newObj;
}

变量定义和内存释放

不同的变量定义方式,会导致变量不能被删除,内存无法释放。

// 定义三个全局变量
var global_var = 1;
global_novar = 2; // 反面教材
(function () {
    global_fromfunc = 3; // 反面教材
}());

// 试图删除
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// 测试该删除
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"

很明显,通过var定义的变量无法被释放。

垃圾回收与内存泄漏

垃圾回收(Garbage Collection),简称GC。简单来讲,GC就是把内存中不需要的数据释放了,这样这部分内存就可以存放其他东西了。在javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。具体回收策略包括以下3种:

标记回收

当从window节点遍历DOM树不能遍历到某个对象,那么这个对象就会被标记为没用的对象。由于回收机制是周期性执行的,这样,当下一个回收周期到来时,这个对象对应的内存就会被释放。

引用计数

当系统中定义了一个对象后,对于这一块内存,javascript会记录有多少个引用指向个部分内存,如果这个数为零,则这部分内存会在下一个回收周期被释放。

手动释放

就好比上一个例子中,利用delete关键字删除变量或属性,达到释放内存的目的。分一下几种情况:

//释放一个对象
obj = null;

//释放是个对象属性
delete obj.propertyName;
delete globalVariable;  //没有用var声明的变量是window的属性,用delete释放。

//释放数组
array.length = 0;

//释放数组元素
array.splice(2,2);  //删除并释放第三个元素起的2个元素

不过需要注意的是, 这几个GC策略是同时作用的:

var o1 = {};  //开辟一块内存放置对象,并用o1指向它
var o2 = o1;  //o2指向与o1同一个内存区域
console.log(o1);  //{}
console.log(o2);  //{}
o2 = null;  //标记o2为没用的对象
console.log(o2);  //null
console.log(o1);  //{}  由于还有o1指向这个内存区域,引用计数不为零,所以内存并没有被释放
o1 = null;  //引用计数为0, 内存释放

如果你访问了已经被回收了的内存,会发生不可预计的严重后果。比如一段内存被释放了,可能里面的值就不是原来的值了,你还要拿来用那不是自己找错误吗?更严重的就是你修改了其他程序的数据!!!我们将这样的变量叫做野指针(wild pointer)。为了避免这样的也只能出现,也为了节省计算机资源,我们需要防止内存泄露(memory leak)。

内存泄漏也称作存储渗漏,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。简单来说就是该内存空间使用完毕之后未回收。

内存泄露是每个开发者最终都不得不面对的问题。即便使用自动内存管理的语言,你还是会碰到一些内存泄漏的情况。内存泄露会导致一系列问题,比如:运行缓慢,崩溃,高延迟,甚至一些与其他应用相关的问题。

可能导致内存泄漏的操作

  • 清除所以子元素用innerHTML=""替代removeChild(),因为在sIEve中监测的结果是用removeChild无法有效地释放dom节点。
//反例
var parent = document.getElementById("parent");
var first = parent.firstChild();
while(first){  //循环多次触发reflow,效率太低
    parent.removeChild(first);  //在旧的浏览器上会导致内存泄漏
    first = parent.firstChild();
}

//正解
document.getElementById("parent").innerHTML = “”;
  • 绑定事件的元素是不能在remove时被清理的,应该在remove之前取消事件绑定。不过更好的办法是用事件委托的方式绑定事件。
var ele = document.getElementById("eleID");
ele.onclick = function fun(){
    //Do stuff here
}
//...
ele.onclick = null;  //删除元素前取消所有事件,jQuery中也是在删除节点前利用removeEventListen去除了对应事件
ele.parentNode.removeChild(ele);
  • 意外的全局变量,会使得实际函数结束就应该释放的内存保留下来,造成资源浪费,包括以下两种情况:

在严格模式下编写代码可以避免这个问题

//情况一: 函数中没有用var声明的变量
function fun1(){
    name = "Mary";   //全局变量
}
fun1();

//情况二: 构造函数没用new关键字调用
function Person(name){
    this.name = name;
}
Person("Mary");   //函数内定义全局变量
  • 定时器中的变量定义,会在每次执行函数的时候重复定义变量,产生严重的内存泄漏。
//反例
setInterval(function(){
    var ele = document.getElementById("eleID");  //改代码毎100毫秒会重复定义该引用
    //Do stuff
}, 100);

//正解
setInterval(function(){
    var ele = document.getElementById("eleID");  //改代码毎100毫秒会重复定义该引用
    //Do stuff
    ele = null;
}, 100);
  • 如果闭包的作用域链中保存着一个DOM对象或者ActiveX对象,那么就意味着该元素将无法被销毁:
//反例
//不妨认为这里的上下文是window
function init(){
    var el = document.getElementById('MyElement'); //这是一个DOM元素的引用,非基本类型
    el.onclick = function(){  //el.onclick是function匿名函数的引用
         alert(el.innerHTML);  //funciton中访问了这个作用域以外的DOM元素引用el,导致el不能被释放
    }
 }
 init();

 //正解
 function init(){
    var el = document.getElementById('MyElement'); //这是一个DOM元素的引用,是非基本类型
    var text = el.innerHTML; //字符串,是基本类型,解决alert(el.innerHTML)不能正常工作问题
    el.onclick = function(){  //el.onclick是function匿名函数的引用
         alert(text);  //var声明的text是基本类型,不必释放
    }
    el = null;  //手动释放,但会导致alert(el.innerHTML)不能正常工作
 }
 init();

 //如果函数结尾要return el,用以下方法释放el
 //正解
function init(){
    var el = document.getElementById('MyElement'); //这是一个DOM元素的引用,是非基本类型
    var text = el.innerHTML; //字符串,是基本类型,解决alert(el.innerHTML)不能正常工作问题
    el.onclick = function(){  //el.onclick是function匿名函数的引用
         alert(text);  //var声明的text是基本类型,不必释放
    }
    try{
        return el;
    } finally {
        el = null;  //手动释放,但会导致alert(el.innerHTML)不能正常工作
    }
 }
 init();
  • 通过createElement,createTextNode等方法创建的元素会在写入DOM后被释放
function create() {
    var parent = document.getElementById('parent');
    for (var i = 0; i < 5000; i++) {
        var el = document.createElement('div');
        el.innerHTML = "test";
        gc.appendChild(el);  //这里释放了内存
    }
}
  • 循环引用导致引用计数永远不为0,内存无法释放:
//构成一个循环引用
var o1 = {name: "o1"};
var o2 = {name: "o2"};
o1.pro = o2;
o2.pro = o1;

//这种情况需要手动清理内存,在不需要的时候把对象置为null或删除pro属性

Faremax
1.7k 声望705 粉丝